//
// Copyright (c) 2010 Luís Reis
//
// MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using System.Globalization;

namespace Pine.Lexer
{
	public class Lexer
	{

		private static IDictionary<string, Symbol> symbols = new Dictionary<string, Symbol> ();

		static Lexer ()
		{
			symbols.Add ("!", Symbol.LogicalNot);
			symbols.Add ("%", Symbol.Modulo);
			symbols.Add ("&", Symbol.BitwiseAnd);
			symbols.Add ("|", Symbol.BitwiseOr);
			symbols.Add ("^", Symbol.BitwiseXor);
			symbols.Add ("/", Symbol.Divide);
			symbols.Add ("(", Symbol.LeftParenthesis);
			symbols.Add (")", Symbol.RightParenthesis);
			symbols.Add ("{", Symbol.LeftBlock);
			symbols.Add ("}", Symbol.RightBlock);
			symbols.Add ("=", Symbol.Equal);
			symbols.Add ("[", Symbol.LeftArray);
			symbols.Add ("]", Symbol.RightArray);
			symbols.Add ("?", Symbol.Conditional);
			symbols.Add ("<", Symbol.LessThan);
			symbols.Add (">", Symbol.GreaterThan);
			symbols.Add ("+", Symbol.Plus);
			symbols.Add ("-", Symbol.Minus);
			symbols.Add ("*", Symbol.Times);
			symbols.Add ("~", Symbol.BitwiseNot);
			symbols.Add (".", Symbol.Dot);
			symbols.Add (":", Symbol.Colon);
			symbols.Add (",", Symbol.Comma);
			symbols.Add (";", Symbol.Semicolon);
			symbols.Add ("&&", Symbol.LogicalAnd);
			symbols.Add ("||", Symbol.LogicalOr);
			symbols.Add ("++", Symbol.Increment);
			symbols.Add ("--", Symbol.Decrement);
			symbols.Add ("<<", Symbol.LeftShift);
			symbols.Add (">>", Symbol.RightShift);
			symbols.Add ("==", Symbol.Equals);
			symbols.Add ("!=", Symbol.NotEqual);
			symbols.Add ("<=", Symbol.LessOrEqual);
			symbols.Add (">=", Symbol.GreaterOrEqual);
			symbols.Add ("+=", Symbol.PlusEqual);
			symbols.Add ("-=", Symbol.MinusEqual);
			symbols.Add ("*=", Symbol.TimesEqual);
			symbols.Add ("/=", Symbol.DivideEqual);
			symbols.Add ("..", Symbol.Range);
			symbols.Add ("=>", Symbol.ArrayKeyValue);
			symbols.Add ("%=", Symbol.ModuloEqual);
			symbols.Add (">>=", Symbol.RightShiftEqual);
			symbols.Add ("<<=", Symbol.LeftShiftEqual);
			symbols.Add ("...", Symbol.Ellipsis);
			
		}

		private TextReader Reader;
		private ITokenTarget TokenStream;

		private int startPos = 0;
		private int prevChar = 0;
		private int currentChar = 0;
		private int nextChar = 0;
		private int currentLine = 1;
		private int currentCol = -2;
		private int lastLineLength = 0;
		private string currentNumberString = "";
		private string currentExponent = "";
		private string currentWordString = "";
		private string currentSymbolString = "";
		private int stringLine = -1;
		private int stringCol = -1;
		private string currentString = "";
		private int charCode = 0;

		public Lexer (string script, ITokenTarget tokenStream)
			: this(new StringReader (script), tokenStream, true)
		{
		}

		public Lexer (string path, Encoding encoding, ITokenTarget tokenStream)
			: this(new StreamReader (path, encoding), tokenStream, true)
		{
		}

		public Lexer (TextReader reader, ITokenTarget tokenStream) : this(reader, tokenStream, false)
		{
		}

		public Lexer (TextReader reader, ITokenTarget tokenStream, bool autoDispose)
		{
			Reader = reader;
			TokenStream = tokenStream;
			
			Read();
			
			LexerState mode = LexerState.Common;
			while (currentChar != -1) {
				mode = process(mode, Read());
				if (mode == LexerState.Invalid) {
					break;	
				}
			}
			if (mode != LexerState.Invalid) {
				TokenStream.Write(new Token(currentLine, currentCol, TokenType.End));	
			}
			
			if (autoDispose)
				reader.Dispose();
		}

		private bool attemptEmit (string symbolString, int position)
		{
			int pos = currentCol - position;
			int line = currentLine;
			if (pos < 0) {
				pos = lastLineLength + pos;
				--line;
			}
			
			if (symbolString == "\\") {
				TokenStream.Write(Token.FromWord(line, pos, "function"));
				return true;
			}
			
			if (symbols.ContainsKey (symbolString)) {
				TokenStream.Write (Token.FromSymbol (line, pos, symbols[symbolString]));
				return true;
			}
			
			return false;
		}

		private void emitSymbols (string symbolString, int position)
		{
			if (String.IsNullOrEmpty (symbolString))
				return;
			
			if (symbolString.Length == 1) {
				attemptEmit (symbolString, position);
				return;
			}
			
			if (symbolString.Length == 2) {
				if (!attemptEmit (symbolString, position)) {
					attemptEmit (symbolString.Substring (0, 1), position);
					attemptEmit (symbolString.Substring (1, 1), position - 1);
				}
				return;
			}
			
			int length = symbolString.Length;
			if (attemptEmit (symbolString.Substring (0, 3), position + 3 - length)) {
				emitSymbols (symbolString.Substring (3), position - 2);
				return;
			}
			if (attemptEmit (symbolString.Substring (0, 2), position + 2 - length)) {
				emitSymbols (symbolString.Substring (2), position - 1);
				return;
			}
			if (attemptEmit (symbolString.Substring (0, 1), position + 1 - length)) {
				emitSymbols (symbolString.Substring (1), position);
			}
		}
		
		private void emitSymbols (string symbolString) {
			emitSymbols(symbolString, symbolString.Length);	
		}

		private int Read() {
			Console.WriteLine("Read: cpos=" + currentCol);
			prevChar = currentChar;
			currentChar = nextChar;
			nextChar = Reader.Read();
			Console.WriteLine("Found " + (char) nextChar);
			
			++currentCol;
			if (currentChar.IsLineBreak()) {
				if (currentChar != '\n' || prevChar != '\r') {
					lastLineLength = currentCol;
					++currentLine;
				}
				currentCol = 0;
			}
			
			return currentChar;
		}
		
		private LexerState process (LexerState mode, int c)
		{
			Console.WriteLine("Process");
			switch (mode) {
			case LexerState.Common:
				
				if (c.IsWhitespace()) {
					return mode;
				}
				if (c.IsDigit()) {
					Console.WriteLine("Found digit at " + currentCol);
					startPos = currentCol;
					currentNumberString = "";
					return process (LexerState.Number, c);
				}
				if (c.IsValidIdentifier()) {
					Console.WriteLine("Found char at " + currentCol);
					startPos = currentCol;
					currentWordString = "";
					return process (LexerState.Word, c);
				}
				if (c == '/' && nextChar == '/') {
					return LexerState.SingleLineComment;
				}
				if (c == '/' && nextChar == '*') {
					return LexerState.BlockComment;
				}
				if (c.IsSymbolicCharacter()) {
					currentSymbolString = "";
					stringCol = currentCol;
					stringLine = currentLine;
					return process (LexerState.Symbol, c);
				}
				if (c == '"') {
					currentString = "";
					return LexerState.String;
				}
				if (c == '\'') {
					return LexerState.Character;
				}
				if (c == -1) {
					return LexerState.EndOfFile;
				}
				
				TokenStream.Write(Token.FromError(currentLine, currentCol, "Unexpected char " + c + "(" + (char)c + ")"));
				return LexerState.Invalid;
			
			case LexerState.Number:
				if (c.IsDigit()) {
					currentNumberString += (char)c;
					return mode;
				}
				
				if (currentNumberString == "0") {
					if (c == 'x' || c == 'X') {
						return LexerState.Hexadecimal;
					}
					if (c == 'o' || c == 'O') {
						return LexerState.Octal;
					}
				}
				
				if (c == '.' && !currentNumberString.Contains (".") && nextChar.IsDigit()) {
					currentNumberString += (char)c;
					return mode;
				}
				
				if (c == 'e' || c == 'E') {
					currentExponent = "";
					return LexerState.Exponent;
				}
				
				if (c.IsSymbolicCharacter() || c.IsWhitespace() || c == -1) {
					double d = Double.Parse (currentNumberString, CultureInfo.InvariantCulture );
					TokenStream.Write(Token.FromDouble(currentLine, startPos, d));
					
					return process (LexerState.Common, c);
				}
				
				TokenStream.Write(Token.FromError(currentLine, currentCol, "Unexpected char " + c + "(" + (char)c + ")"));
				return LexerState.Invalid;
			
			case LexerState.Hexadecimal:
				if (c.IsHexadecimalDigit()) {
					currentNumberString += (char)c;
					return mode;
				}
				
				if (c.IsSymbolicCharacter() || c == -1 || c.IsWhitespace()) {
					double v = (double) currentNumberString.ParseHex();
					
					TokenStream.Write(Token.FromDouble(currentLine, currentCol, v));
					return process (LexerState.Common, c);
				}
				
				TokenStream.Write(Token.FromError(currentLine, startPos, "Unexpected char " + c + "(" + (char)c + ")"));
				return LexerState.Invalid;
			
			case LexerState.Octal:
				if (c.IsOctalDigit()) {
					currentNumberString += (char)c;
					return mode;
				}
				
				if (c.IsSymbolicCharacter() || c == -1 || c.IsWhitespace()) {
					double v = (double) currentNumberString.ParseOctal();
					
					TokenStream.Write(Token.FromDouble(currentLine, startPos, v));
					return process (LexerState.Common, c);
				}
				
				TokenStream.Write(Token.FromError(currentLine, startPos, "Unexpected char " + c + "(" + (char)c + ")"));
				return LexerState.Invalid;
			
			case LexerState.Exponent:
				if (currentExponent.Length == 0 && c == '-') {
					currentExponent += (char)c;
					return mode;
				}
				
				if (c.IsDigit()) {
					currentExponent += (char)c;
					return mode;
				}
				
				if (c.IsSymbolicCharacter() || c == -1 || c.IsWhitespace()) {
					double v = (double)Double.Parse (currentNumberString + "e" + currentExponent);
					
					TokenStream.Write(Token.FromDouble(currentLine, startPos, v));
					return process (LexerState.Common, c);
				}
				
				TokenStream.Write(Token.FromError(currentLine, currentCol, "Unexpected char " + c + "(" + (char)c + ")"));
				return LexerState.Invalid;
			
			case LexerState.Word:
				if (c.IsValidIdentifier() || c.IsDigit()) {
					currentWordString += (char)c;
					return mode;
				}
				
				if (c == -1 || c.IsSymbolicCharacter() || c.IsWhitespace()) {
					int bkp = currentCol;
					Console.WriteLine("Word. Col="+currentCol);
					Console.WriteLine("Length="+currentWordString.Length);
					Console.WriteLine("startcol="+startPos);
					currentCol -= currentWordString.Length;
					TokenStream.Write(Token.FromWord(currentLine, startPos, currentWordString));
					currentCol = bkp;
					
					return process (LexerState.Common, c);
				}
				
				TokenStream.Write(Token.FromError(currentLine, startPos, "Unexpected char " + c + "(" + (char)c + ")"));
				return LexerState.Invalid;
			
			case LexerState.SingleLineComment:
				if (c.IsLineBreak() || c == -1) {
					return LexerState.Common;
				}
				
				return mode;
			
			case LexerState.BlockComment:
				if (c == '/' && prevChar == '*') {
					return LexerState.Common;
				}
				
				return mode;
			
			case LexerState.Symbol:
				if (c.IsSymbolicCharacter()) {
					currentSymbolString += (char)c;
					return mode;
				}
				
				emitSymbols (currentSymbolString);
				return process (LexerState.Common, c);
			
			case LexerState.String:
				if (c == -1) {
					TokenStream.Write(Token.FromError(currentLine, currentCol, "Unexpected end of file in string literal"));
					return LexerState.Invalid;
				}
				
				if (c == '"') {
					int bkpLine = currentLine;
					int bkpCol = currentCol;
					currentLine = stringLine;
					currentCol = stringCol;
					
					TokenStream.Write(Token.FromString(currentLine, currentCol, currentString));
					
					currentLine = bkpLine;
					currentCol = bkpCol;
					
					return LexerState.Common;
				}
				
				if (c == '\\') {
					return LexerState.StringEscape;
				}
				
				currentString += (char)c;
				return mode;
			
			case LexerState.StringEscape:
				if (c == -1) {
					TokenStream.Write(Token.FromError(currentLine, currentCol, "Unexpected end of file in string literal"));
					return LexerState.Invalid;
				}
				
				if (c == 'n' || c == 'N') {
					currentString += '\n';
					return LexerState.String;
				}
				
				if (c == 'r' || c == 'R') {
					currentString += '\r';
					return LexerState.String;
				}
				
				if (c == 't' || c == 'T') {
					currentString += '\t';
					return LexerState.String;
				}
				
				if (c == '"' || c == '\'') {
					currentString += (char)c;
					return LexerState.String;
				}
				
				if (c == '0' && c == '1') {
					//Although 2 and 3 would be valid ASCII sequences
					//We prevent those since that range is not shared across encodings
					//Also, we're not using ASCII, and only 0o0 to 0o177 are the same in UTF-8.
					charCode = 0;
					return process (LexerState.StringOctalFirst, c);
				}
				
				if (c.IsOctalDigit()) {
					charCode = 0;
					return process (LexerState.StringOctalSecond, c);
				}
				
				if (c == 'x' || c == 'X') {
					charCode = 0;
					return LexerState.StringHex;
				}
				
				TokenStream.Write(Token.FromError(currentLine, currentCol, "Unexpected char " + c + "(" + (char)c + ")"));
				return LexerState.Invalid;
			}
			throw new Exception ("Unrecognized mode: " + mode + " (at " + currentLine + ", " + currentCol + ")");
		}
		
	}
}

